from math import tanh
from pysqlite2 import dbapi2 as sqlite

#TODO: Este codigo fue hecho con prisa, medio dormido, en el ultimo dia de la entrega
# y necesita ser refactorizado, es probable que algunas partes no tengan sentido
# y que la documentacion no diga nada asombroso(si es que dice algo)

def dtanh(y):
    return 1.0-y*y

class skynet:
    def __init__(self, dbname):
        self.con = sqlite.connect(dbname)
        self.HIDDENWORD = 0
        self.HIDDENURL = 1
        self.layers = {self.HIDDENWORD:'hiddenword',
                       self.HIDDENURL:'hiddenurl'}
    def __del__(self):
        self.con.close()
        
    def create_tables(self):
        self.con.execute("create table hiddennode(create_key)")
        self.con.execute("create table hiddenword(fromid, toid, strength)")
        self.con.execute("create table hiddenurl(fromid, toid, strength)")
        self.con.commit()

    def get_strength(self, fromid, toid, layer):
        '''Gets the strength of any given layer'''
        table = self.layers[layer]
        query = "select strength from %s where fromid=%d and toid=%d" % (table, fromid, toid)
        cursor = self.con.execute(query)
        if cursor is None:
            # Return a default strength of -0.2 for the hiddenword layer
            # and a default strength of 0.0 for the hiddenurl layer
            return -0.2 if layer == 0 else 0.0
        else: 
            result = cursor.fetchone()
            print "RESULT IS", result
            return result[0]
    def set_strength(self, fromid, toid, layer, strength):
        '''Sets the strength in any given layer'''
        table = self.layers[layer]
        query = "select rowid from %s where fromid=%d and toid=%d" % (table, fromid, toid)
        result = self.con.execute(query).fetchone()
        # If the item already exists, update the info
        if result:
            rowid = result[0]
            query = "update %s set strength=%f where rowid=%d" % (table, strength, rowid)
            self.con.execute(query)
            return rowid
        # If the item does not exist, create the row
        else:
            query = "insert into %s (fromid, toid, strength) values (%d,%d,%f)" % (table, fromid, toid, strength)
            cursor = self.con.execute(query)
            return cursor.lastrowid
    def generate_hiddennode(self, wordids, urlids):
        if len(wordids) > 3: return None # TODO: Porque 3 ? namas, pues
        # Check if there is already a node for these words
        create_key = '_'.join(sorted(map(str, wordids)))
        query = "select rowid from hiddennode where create_key='%s'" % create_key
        result = self.con.execute(query).fetchone()
        # If the node does not exist, create it
        if result is None:
            query = "insert into hiddennode (create_key) values ('%s')" % create_key
            cursor = self.con.execute(query)
            hiddenid = cursor.lastrowid
            # Put some default weights
            wordstrength = 1.0/len(wordids)
            urlstrength = 0.1 # TODO: Just because I want?
            for wordid in wordids:
                self.set_strength(wordid, hiddenid, self.HIDDENWORD, wordstrength)
            for urlid in urlids:
                self.set_strength(hiddenid, urlid, self.HIDDENURL, urlstrength)
    
    def get_all_hiddenids(self, wordids, urlids):
        hidden = set()
        for wordid in wordids:
            query = "select toid from hiddenword where fromid=%d" % wordid
            cursor = self.con.execute(query)
            hidden.update([row[0] for row in cursor.fetchall()])
        for urlid in urlids:
            query = "select fromid from hiddenurl where toid=%d" % urlid
            cursor = self.con.execute(query)
            hidden.update([row[0] for row in cursor.fetchall()])
        return list(hidden)
        
    def setup_network(self, wordids, urlids):
        ''' Sets up the neural network initial values and matrices'''
        # Value lists
        self.wordids = wordids
        self.hiddenids = self.get_all_hiddenids(wordids, urlids)
        print self.hiddenids
        self.urlids = urlids
        # Node outputs
        self.ai = [1.0] * len(self.wordids)
        self.ah = [1.0] * len(self.hiddenids)
        self.ao = [1.0] * len(self.urlids)
        # Create the weight matrix
        self.wi = [[self.get_strength(wordid, hiddenid, self.HIDDENWORD)
                    for hiddenid in self.hiddenids]
                    for wordid in self.wordids]
        self.wo = [[self.get_strength(hiddenid, urlid, self.HIDDENURL)
                    for urlid in self.urlids]
                    for hiddenid in self.hiddenids]
        
    def feedforward(self):
        # the only inputs are the query words
        for i in range(len(self.wordids)):
            self.ai[i] = 1.0
        
        # hidden activations
        for j in range(len(self.hiddenids)):
            sum = 0.0
            for i in range(len(self.wordids)):
                sum = sum + self.ai[i] * self.wi[i][j]
            self.ah[j] = tanh(sum)
        
        # output activations
        for k in range(len(self.urlids)):
            sum = 0.0
            for j in range(len(self.hiddenids)):
                sum = sum + self.ah[j] * self.wo[j][k]
            self.ao[k] = tanh(sum)
        return self.ao[:]
    
    def getresult(self,wordids,urlids):
        self.setup_network(wordids,urlids)
        return self.feedforward()
    
    def backPropagate(self, targets, N=0.5):
        # calculate errors for output
        output_deltas = [0.0] * len(self.urlids)
        for k in range(len(self.urlids)):
            error = targets[k]-self.ao[k]
            output_deltas[k] = dtanh(self.ao[k]) * error
        # calculate errors for hidden layer
        hidden_deltas = [0.0] * len(self.hiddenids)
        for j in range(len(self.hiddenids)):
            error = 0.0
            for k in range(len(self.urlids)):
                error = error + output_deltas[k]*self.wo[j][k]
            hidden_deltas[j] = dtanh(self.ah[j]) * error
            
        # update output weights
        for j in range(len(self.hiddenids)):
            for k in range(len(self.urlids)):
                change = output_deltas[k]*self.ah[j]
                self.wo[j][k] = self.wo[j][k] + N*change
        # update input weights
        for i in range(len(self.wordids)):
            for j in range(len(self.hiddenids)):
                change = hidden_deltas[j]*self.ai[i]
                self.wi[i][j] = self.wi[i][j] + N*change
                
    def trainquery(self,wordids,urlids,selectedurl):
        # generate a hidden node if necessary
        self.generate_hiddennode(wordids,urlids)
        self.setup_network(wordids,urlids)
        self.feedforward()
        targets=[0.0]*len(urlids)
        targets[urlids.index(selectedurl)]=1.0
        error = self.backPropagate(targets)
        self.updatedatabase()
        
    def updatedatabase(self):
        # set them to database values
        for i in range(len(self.wordids)):
            for j in range(len(self.hiddenids)):
                self.set_strength(self.wordids[i],self. hiddenids[j],0,self.wi[i][j])
        for j in range(len(self.hiddenids)):
            for k in range(len(self.urlids)):
                self.set_strength(self.hiddenids[j],self.urlids[k],1,self.wo[j][k])
        self.con.commit()